import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
import * as crypto from 'crypto';
import * as parser from '@babel/parser';
import traverse from '@babel/traverse';
import { parse as vueParse } from '@vue/compiler-sfc';

interface PathAliasConfig {
    baseUrl: string;
    paths: { [key: string]: string[] };
}

interface AliasConfig {
    [alias: string]: string;
}

// Cache for dependency analysis
interface CacheEntry {
    hash: string;
    dependencies: Set<string>;
    lineCount: number;
}

interface GraphCacheEntry {
    hash: string;
    graph: [string, string[]][];
}

let analysisCache: vscode.Memento | undefined;

export function initializeAnalyzer(context: vscode.ExtensionContext) {
    analysisCache = context.workspaceState;
}

function getPathAliasConfig(projectRoot: string): PathAliasConfig | null {
    const tsconfigPath = path.join(projectRoot, 'tsconfig.json');
    const jsconfigPath = path.join(projectRoot, 'jsconfig.json');
    console.log('getPathAliasConfig tsconfigPath =====>>>>>> ', tsconfigPath);
    console.log('getPathAliasConfig jsconfigPath =====>>>>>> ', jsconfigPath);

    console.log('fs.existsSync(tsconfigPath) ', fs.existsSync(tsconfigPath));

    const configPath = fs.existsSync(tsconfigPath)
        ? tsconfigPath
        : fs.existsSync(jsconfigPath)
        ? jsconfigPath
        : null;

    if (!configPath) {
        return null;
    }

    try {
        const configFileContent = fs.readFileSync(configPath, 'utf-8');
        console.log('configFileContent =====>>>>>> ', configFileContent);
        // A simple regex to remove comments before parsing
        const jsonContent = JSON.parse(configFileContent.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, ''));
        console.log('jsonContent =====>>>>>> ', jsonContent);

        const compilerOptions = jsonContent.compilerOptions;
        console.log('compilerOptions =====>>>>>> ', compilerOptions);

        if (compilerOptions && compilerOptions.paths) {
            return {
                baseUrl: path.resolve(projectRoot, compilerOptions.baseUrl || '.'),
                paths: compilerOptions.paths,
            };
        }
    } catch (e) {
        console.error(`Error parsing alias config file at ${configPath}:`, e);
    }

    return null;
}

function findProjectRoot(startPath: string): string | null {
    let currentDir = path.dirname(startPath);
    while (currentDir !== path.dirname(currentDir)) {
        const packageJsonPath = path.join(currentDir, 'package.json');
        if (fs.existsSync(packageJsonPath)) {
            return currentDir;
        }
        currentDir = path.dirname(currentDir);
    }
    return null;
}

function getVueCLI2AliasConfig(projectRoot: string): AliasConfig {
    // Simplified alias config for now.
    return {
        '@': path.join(projectRoot, 'src')
    };
}

function resolvePath(importPath: string, currentDir: string, aliasConfig: PathAliasConfig | null): string | null {
    // 1. Resolve using path aliases
    console.log('aliasConfig =====>>>>>> ', aliasConfig);
    if (aliasConfig) {
        for (const alias in aliasConfig.paths) {
            const aliasPattern = alias.replace('*', '');
            if (importPath.startsWith(aliasPattern)) {
                for (const p of aliasConfig.paths[alias]) {
                    const resolvedAliasPath = p.replace('*', '');
                    const remainingPath = importPath.substring(aliasPattern.length);
                    const absolutePath = path.resolve(aliasConfig.baseUrl, resolvedAliasPath + remainingPath);
                    const resolvedFile = tryResolveExtensions(absolutePath);
                    if (resolvedFile) {
                        return resolvedFile;
                    }
                }
            }
        }
    }

    // 2. Fallback to relative path resolution
    const relativeResolved = tryResolveExtensions(path.resolve(currentDir, importPath));
    if (relativeResolved) {
        return relativeResolved;
    }

    return null;
}

function tryResolveExtensions(filePath: string): string | null {
    const extensions = ['.js', '.jsx', '.ts', '.tsx', '.vue', '.svelte'];
    
    // Check as a file
    try {
        if (fs.existsSync(filePath) && fs.lstatSync(filePath).isFile()) {
            return fs.realpathSync(filePath);
        }
    } catch (e) { /* suppress */ }

    // Check with extensions
    for (const ext of extensions) {
        const pathWithExt = filePath + ext;
         try {
            if (fs.existsSync(pathWithExt) && fs.lstatSync(pathWithExt).isFile()) {
                return fs.realpathSync(pathWithExt);
            }
         } catch (e) { /* suppress */ }
    }

    // Check as a directory (resolving to index file)
    try {
      if (fs.existsSync(filePath) && fs.lstatSync(filePath).isDirectory()) {
          for (const ext of extensions) {
              const indexFile = path.join(filePath, 'index' + ext);
              try {
                if (fs.existsSync(indexFile) && fs.lstatSync(indexFile).isFile()) {
                    return fs.realpathSync(indexFile);
                }
              } catch (e) { /* suppress */ }
          }
      }
    } catch(e) { /* suppress */ }

    return null;
}

function getDirectDependencies(filePath: string, aliasConfig: PathAliasConfig | null): { dependencies: Set<string>, lineCount: number, parseFailed: boolean } {
    if (filePath.startsWith('missing:')) {
        return { dependencies: new Set(), lineCount: 0, parseFailed: false };
    }

    try {
        const code = fs.readFileSync(filePath, 'utf-8');
        const hash = crypto.createHash('md5').update(code).digest('hex');
        const cached = analysisCache?.get<CacheEntry>(filePath);

        if (cached && cached.hash === hash) {
            return { dependencies: new Set(cached.dependencies), lineCount: cached.lineCount, parseFailed: false };
        }

        const dependencies = new Set<string>();
        const lineCount = code.split('\n').length;
        const currentDir = path.dirname(filePath);
        let parseFailed = false;

        try {
            let ast;
            if (filePath.endsWith('.vue')) {
                const { descriptor } = vueParse(code);
                const script = descriptor.script?.content || descriptor.scriptSetup?.content;
                if (!script) {
                    analysisCache?.update(filePath, { hash, dependencies: [], lineCount });
                    return { dependencies, lineCount, parseFailed };
                }
                ast = parser.parse(script, { sourceType: 'module', plugins: ['typescript', 'jsx'] });
            } else {
                ast = parser.parse(code, { sourceType: 'module', plugins: ['typescript', 'jsx'] });
            }

            traverse(ast, {
                ImportDeclaration({ node }) {
                    const importPath = node.source.value;
                    const resolvedPath = resolvePath(importPath, currentDir, aliasConfig);
                    if (resolvedPath) {
                        dependencies.add(resolvedPath);
                    } else {
                        dependencies.add(`missing:${importPath}`);
                    }
                },
                CallExpression({ node }) {
                    if (node.callee.type === 'Import') { 
                        const arg = node.arguments[0];
                        if (arg?.type === 'StringLiteral') {
                            const importPath = arg.value;
                            const resolvedPath = resolvePath(importPath, currentDir, aliasConfig);
                            if (resolvedPath) {
                                dependencies.add(resolvedPath);
                            } else {
                                dependencies.add(`missing:${importPath}`);
                            }
                        } else if (
                            arg?.type === 'TemplateLiteral' &&
                            arg.quasis.length === 1 &&
                            arg.quasis[0].type === 'TemplateElement'
                        ) {
                            const importPath = arg.quasis[0].value.raw;
                            const resolvedPath = resolvePath(importPath, currentDir, aliasConfig);
                            if (resolvedPath) {
                                dependencies.add(resolvedPath);
                            } else {
                                dependencies.add(`missing:${importPath}`);
                            }
                        }
                    }
                },
            });
        } catch (error) {
            console.error(`Failed to parse ${filePath}:`, error);
            parseFailed = true;
        }

        if (!parseFailed) {
            analysisCache?.update(filePath, { hash, dependencies: Array.from(dependencies), lineCount });
        }

        return { dependencies, lineCount, parseFailed };

    } catch (e) {
        if (e instanceof Error) {
            console.error(`Error reading file ${filePath}: ${e.message}`);
        }
        return { dependencies: new Set(), lineCount: 0, parseFailed: true };
    }
}

function getForwardDependencies(entryPoint: string, aliasConfig: PathAliasConfig | null): Map<string, Set<string>> {
    const graph = new Map<string, Set<string>>();
    const { dependencies } = getDirectDependencies(entryPoint, aliasConfig);
    graph.set(entryPoint, dependencies);
    return graph;
}

async function getWorkspaceFiles(options: { include?: string[], exclude?: string[] }): Promise<string[]> {
    const config = vscode.workspace.getConfiguration('component-dependency-visualizer');
    // Respect the user's settings, but provide a default if they are not set
    const include = options.include || config.get<string[]>('include');
    const exclude = options.exclude || config.get<string[]>('exclude');

    const includePatterns = include && include.length > 0 ? include : ['**/*.{js,jsx,ts,tsx,vue}'];
    const excludePatterns = exclude && exclude.length > 0 ? exclude : ['**/node_modules/**', '**/dist/**'];

    const includeGlob = includePatterns.length > 1 ? `{${includePatterns.join(',')}}` : includePatterns[0];
    const excludeGlob = excludePatterns.length > 1 ? `{${excludePatterns.join(',')}}` : excludePatterns[0];
    
    const files = await vscode.workspace.findFiles(includeGlob, excludeGlob);
    return files.map(file => file.fsPath);
}

async function findReferences(entryPoint: string, aliasConfig: PathAliasConfig | null, options: { include?: string[], exclude?: string[] }): Promise<Map<string, Set<string>>> {
    const normalizedEntryPoint = fs.realpathSync(entryPoint);
    const references = new Map<string, Set<string>>();
    const allFiles = await getWorkspaceFiles(options);

    for (const file of allFiles) {
        if (file === normalizedEntryPoint) continue;

        const { dependencies } = getDirectDependencies(file, aliasConfig);

        if (dependencies.has(normalizedEntryPoint)) {
            references.set(file, new Set([normalizedEntryPoint]));
        }
    }
    return references;
}

function detectCycles(graph: Map<string, Set<string>>): Set<string> {
    const allNodes = Array.from(graph.keys());
    const inCycle = new Set<string>();
    
    for (const startNode of allNodes) {
        const visiting = new Set<string>();
        const path = new Set<string>();
        
        function dfs(currentNode: string) {
            visiting.add(currentNode);
            path.add(currentNode);

            const dependencies = graph.get(currentNode) || new Set();
            for (const dependency of dependencies) {
                if (inCycle.has(dependency)) continue;

                if (path.has(dependency)) {
                    // Cycle detected
                    for (const node of path) {
                        if (node !== dependency) {
                           const subPath = Array.from(path).slice(Array.from(path).indexOf(dependency));
                           subPath.forEach(n => inCycle.add(n));
                        }
                    }
                    inCycle.add(dependency);
                }
                
                if (!visiting.has(dependency)) {
                    dfs(dependency);
                }
            }
            path.delete(currentNode);
        }

        if (!visiting.has(startNode)) {
            dfs(startNode);
        }
    }
    return inCycle;
}

async function createD3Data(graph: Map<string, Set<string>>, entryPoint: string, aliasConfig: PathAliasConfig | null, largeComponentThreshold: number, highlightCycles: boolean, projectRoot: string) {
    const d3Data: { nodes: any[], links: any[] } = { nodes: [], links: [] };
    const allNodes = new Set<string>();
    
    for (const [key, values] of graph.entries()) {
        allNodes.add(key);
        for (const value of values) {
            allNodes.add(value);
        }
    }

    const nodesInCycle = highlightCycles ? detectCycles(graph) : new Set<string>();

    const fileDetailsCache = new Map<string, { lineCount: number, parseFailed: boolean, dependencyCount: number }>();

    const getFileDetails = (filePath: string) => {
        if (fileDetailsCache.has(filePath)) {
            return fileDetailsCache.get(filePath)!;
        }
        if (filePath.startsWith('missing:')) {
            const details = { lineCount: 0, parseFailed: false, dependencyCount: 0 };
            fileDetailsCache.set(filePath, details);
            return details;
        }
        const { lineCount, parseFailed, dependencies } = getDirectDependencies(filePath, aliasConfig);
        const details = { lineCount, parseFailed, dependencyCount: dependencies.size };
        fileDetailsCache.set(filePath, details);
        return details;
    };

    const formatPathToAlias = (filePath: string, aliasConfig: PathAliasConfig | null, projectRoot: string): string => {
        try {
            if (filePath.startsWith('missing:')) {
                return filePath.substring(8);
            }
    
            if (!aliasConfig || !aliasConfig.paths) {
                return path.relative(projectRoot, filePath);
            }
    
            const rules = Object.entries(aliasConfig.paths)
                .map(([alias, realPaths]) => {
                    if (!Array.isArray(realPaths)) {
                        console.warn(`[Code Spider] Alias value for "${alias}" is not an array. Skipping.`);
                        return [];
                    }
                    return realPaths.map(p => ({
                        alias: alias,
                        realPath: path.resolve(aliasConfig.baseUrl, p.replace('*', '')),
                    }));
                })
                .flat()
                .sort((a, b) => b.realPath.length - a.realPath.length);
    
            for (const rule of rules) {
                if (filePath.startsWith(rule.realPath)) {
                    const remainingPath = filePath.substring(rule.realPath.length);
                    const finalAlias = rule.alias.replace('*', remainingPath.startsWith('/') ? remainingPath.substring(1) : remainingPath);
                    return finalAlias;
                }
            }
    
            return path.relative(projectRoot, filePath);

        } catch (error) {
            console.error(`[Code Spider] Error in formatPathToAlias for path: ${filePath}. Returning original path.`, error);
            return path.basename(filePath);
        }
    };

    const depths = new Map<string, number>();
    const queue: [string, number][] = [[entryPoint, 0]];
    const visited = new Set<string>([entryPoint]);
    depths.set(entryPoint, 0);

    while(queue.length > 0) {
        const [current, depth] = queue.shift()!;
        const deps = graph.get(current) || new Set();
        for (const dep of deps) {
            if (!visited.has(dep)) {
                visited.add(dep);
                depths.set(dep, depth + 1);
                queue.push([dep, depth + 1]);
            }
        }
    }
    
    for(const [node, deps] of graph.entries()) {
        if(deps.has(entryPoint) && !depths.has(node)) {
             depths.set(node, -1);
        }
    }

    for (const filePath of allNodes) {
        let type = 'child';
        if (filePath === entryPoint) {
            type = 'entry';
        } else if (graph.has(filePath) && graph.get(filePath)!.has(entryPoint)) {
            type = 'parent';
        }

        if (!d3Data.nodes.some(n => n.id === filePath)) {
            const { lineCount, parseFailed, dependencyCount } = getFileDetails(filePath);
            let codePreview = '';
            if (!filePath.startsWith('missing:')) {
                try {
                    const fileContent = fs.readFileSync(filePath, 'utf-8');
                    codePreview = fileContent.split('\n').slice(0, 10).join('\n');
                } catch (e) {
                    codePreview = 'Could not read file content.';
                }
            }

            d3Data.nodes.push({
                id: filePath,
                name: formatPathToAlias(filePath, aliasConfig, projectRoot),
                path: filePath,
                type: type,
                lineCount: lineCount,
                dependencyCount: dependencyCount,
                isLarge: lineCount > largeComponentThreshold,
                parseFailed: parseFailed,
                isMissing: filePath.startsWith('missing:'),
                codePreview: codePreview,
                depth: depths.get(filePath),
                inCycle: nodesInCycle.has(filePath)
            });
        }
    }
    
    for (const [source, targets] of graph.entries()) {
        for (const target of targets) {
            d3Data.links.push({
                source: source,
                target: target,
                inCycle: nodesInCycle.has(source) && nodesInCycle.has(target)
            });
        }
    }
    
    const referencedByMap = new Map<string, number>();
    d3Data.links.forEach(link => {
        referencedByMap.set(link.target, (referencedByMap.get(link.target) || 0) + 1);
    });

    d3Data.nodes.forEach(node => {
        node.referencedByCount = referencedByMap.get(node.id) || 0;
    });

    return d3Data;
}

export async function analyzeDependencies(entryPoint: string, options: { include?: string[], exclude?: string[] }) {
    const startTime = Date.now();
    console.log('entryPoint =====>>>>>> ', entryPoint);
    const projectRoot = findProjectRoot(entryPoint);
    if (!projectRoot) {
        vscode.window.showErrorMessage('Could not find project root (package.json).');
        return { d3Data: null, stats: { nodeCount: 0, analysisTime: 0 } };
    }
    
    const aliasConfig = getPathAliasConfig(projectRoot);
    console.log('analyzeDependencies aliasConfig =====>>>>>> ', aliasConfig);
    
    // --- Caching Logic ---
    const allFiles = await getWorkspaceFiles(options);
    const combinedHash = allFiles
        .map(file => fs.statSync(file).mtime.getTime())
        .join('');
    const cacheKey = `graph-cache:${projectRoot}`;
    const cachedData = analysisCache?.get<GraphCacheEntry>(cacheKey);

    if (cachedData && cachedData.hash === combinedHash) {
        console.log("Using cached graph data.");
        const cachedGraph = new Map<string, Set<string>>(cachedData.graph.map(([key, value]) => [key, new Set(value)]));
        const configuration = vscode.workspace.getConfiguration('component-dependency-visualizer');
        const largeComponentThreshold = configuration.get<number>('largeComponentThreshold') || 500;
        const highlightCycles = configuration.get<boolean>('highlightCycles') ?? true;
        const d3Data = await createD3Data(cachedGraph, entryPoint, aliasConfig, largeComponentThreshold, highlightCycles, projectRoot);
        return {
            d3Data,
            stats: { nodeCount: cachedGraph.size, analysisTime: Date.now() - startTime }
        };
    }

    console.log("No valid cache found, performing full analysis.");
    // --- End Caching Logic ---

    const forwardDeps = getForwardDependencies(entryPoint, aliasConfig);
    const references = await findReferences(entryPoint, aliasConfig, options);

    const mergedGraph = new Map<string, Set<string>>(forwardDeps);
    for (const [key, value] of references.entries()) {
        if (mergedGraph.has(key)) {
            value.forEach(v => mergedGraph.get(key)!.add(v));
        } else {
            mergedGraph.set(key, value);
        }
    }

    // --- Caching Logic ---
    const serializableGraph: [string, string[]][] = Array.from(mergedGraph.entries()).map(([key, value]) => [key, Array.from(value)]);
    analysisCache?.update(cacheKey, { hash: combinedHash, graph: serializableGraph });
    // --- End Caching Logic ---

    const configuration = vscode.workspace.getConfiguration('component-dependency-visualizer');
    const largeComponentThreshold = configuration.get<number>('largeComponentThreshold') || 500;
    const highlightCycles = configuration.get<boolean>('highlightCycles') ?? true;

    const d3Data = await createD3Data(mergedGraph, entryPoint, aliasConfig, largeComponentThreshold, highlightCycles, projectRoot);
    
    const analysisTime = Date.now() - startTime;
    console.log(`[Code Spider] Analysis finished in ${analysisTime}ms`);

    console.log('[Code Spider] Final D3 data being sent to webview:', JSON.stringify(d3Data, null, 2));

    return {
        d3Data: d3Data,
        nodeInfo: {},
        stats: { nodeCount: mergedGraph.size, analysisTime }
    };
}